Repository Public
1import type { Metadata } from "next";2import { Suspense } from "react";3import { NodesExplorer } from "@/components/nodes/NodesExplorer";4import { PageIntro, SectionHead } from "@/components/ui/Section";5import { nodes, planned, timeline } from "@/lib/data";6import { formatDate, formatNumber, getDict, type Locale } from "@/lib/i18n";7import { aggregate } from "@/lib/inventory";8import { pageMetadata } from "@/lib/seo";910type Props = { params: Promise<{ locale: Locale }> };1112export async function generateMetadata({ params }: Props): Promise<Metadata> {13 return pageMetadata((await params).locale, "nodes", "/nodes");14}1516export default async function NodesPage({ params }: Props) {17 const { locale } = await params;18 const t = getDict(locale);19 const n = t.nodes;20 const agg = aggregate(nodes);21 const stats = [22 { v: formatNumber(agg.documented, locale), l: n.stats.machines },23 { v: agg.unifiedMemoryGB !== null ? `${formatNumber(agg.unifiedMemoryGB, locale)} ${t.common.gb}` : t.common.notDocumented, l: n.stats.memory },24 { v: agg.cpuCores !== null ? formatNumber(agg.cpuCores, locale) : t.common.notDocumented, l: n.stats.cpu },25 { v: agg.lastVerifiedAt ? formatDate(agg.lastVerifiedAt, locale) : t.common.notDocumented, l: n.stats.verified },26 ];27 return (28 <>29 <PageIntro eyebrow={n.eyebrow} title={n.title} intro={n.intro} />30 <section className="shell">31 <ul className="grid grid-cols-2 gap-3 lg:grid-cols-4" data-testid="inventory-stats">32 {stats.map((s) => (33 <li key={s.l} className="card p-4 md:p-5">34 <p>35 <span className="num block text-xl font-semibold tracking-[-0.03em] md:text-3xl">{s.v}</span>{" "}36 <span className="label mt-2 block">{s.l}</span>37 </p>38 </li>39 ))}40 </ul>41 <div className="mt-6">42 <Suspense fallback={<p className="card p-10 text-center text-mute">…</p>}>43 <NodesExplorer nodes={nodes} locale={locale} />44 </Suspense>45 </div>46 </section>4748 <section className="shell mt-24 md:mt-36">49 <SectionHead eyebrow={n.timeline.eyebrow} title={n.timeline.title} intro={n.timeline.intro} />50 <ol className="mt-12 border-l border-line-strong md:ml-[25%]">51 {timeline.map((e) => (52 <li key={e.date + e.title.en} className="reveal relative pb-10 pl-7 last:pb-0">53 <span aria-hidden="true" className="absolute -left-[4.5px] top-1.5 h-2 w-2 rounded-full border-2 border-accent bg-bg" />54 <time dateTime={e.date} className="num font-mono text-[0.75rem] tracking-[0.06em] text-accent">{formatDate(e.date, locale)}</time>55 <h3 className="mt-2 text-xl font-medium tracking-[-0.02em]">{e.title[locale]}</h3>56 <p className="mt-2 max-w-[66ch] text-[0.9375rem] leading-relaxed text-body">{e.detail[locale]}</p>57 </li>58 ))}59 </ol>60 </section>6162 <section className="shell mt-24 md:mt-36">63 <SectionHead eyebrow={n.planned.eyebrow} title={n.planned.title} />64 <div className="mt-10 md:ml-[25%]">65 {planned.length === 0 ? (66 <p className="card max-w-[70ch] border-dashed p-6 text-[0.9375rem] leading-relaxed text-body">{n.planned.empty}</p>67 ) : (68 <ul className="divide-y divide-line border-y border-line">69 {planned.map((p) => (70 <li key={p.label.en} className="py-5">71 <p className="font-medium">{p.label[locale]} <span className="label ml-3 !text-accent">{t.common.lifecycle.planned}</span></p>72 <p className="mt-1 text-[0.9375rem] text-body">{p.note[locale]}</p>73 </li>74 ))}75 </ul>76 )}77 </div>78 </section>79 </>80 );81}82